Содержание

  • 1  Подготовка данных
  • 2  Исследование задачи
    • 2.1  Модель дерева решений для задачи классификации при дисбалансе классов
    • 2.2  Модель случайного леса для задачи классификации при дисбалансе классов
    • 2.3  Модель логистической регрессии для задачи классификации при дисбалансе классов
  • 3  Борьба с дисбалансом
    • 3.1  Модель дерева решений для задачи классификации при сбалансированной выборке
    • 3.2  Модель случайного леса для задачи классификации при сбалансированной выборке
    • 3.3  Модель логистической регрессии для задачи классификации при сбалансированной выборке.
  • 4  Тестирование модели
  • 5  Вывод
  • 6  Чек-лист готовности проекта

Отток клиентов¶

Из «Бета-Банка» стали уходить клиенты. Каждый месяц. Немного, но заметно. Банковские маркетологи посчитали: сохранять текущих клиентов дешевле, чем привлекать новых.

Нужно спрогнозировать, уйдёт клиент из банка в ближайшее время или нет. Вам предоставлены исторические данные о поведении клиентов и расторжении договоров с банком.

Постройте модель с предельно большим значением F1-меры. Чтобы сдать проект успешно, нужно довести метрику до 0.59. Проверьте F1-меру на тестовой выборке самостоятельно.

Дополнительно измеряйте AUC-ROC, сравнивайте её значение с F1-мерой.

Источник данных: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling

Цели и задачи проекта¶

Цели:

Провести исследование с целью прогнозирования ухода клиентов из «Бета-Банка» в ближайшее время.

Результаты исследования позволят маркетологам сохранить текущих клиентов, т.к. это дешевле, чем привлекать новых.

Задачи:

  1. Загрузим и подготовим данные. Поясним порядок действий.
  2. Исследуем баланс классов, обучим модель без учёта дисбаланса. Кратко опишем выводы.
  3. Улучшим качество модели, учитывая дисбаланс классов. Обучим разные модели и найдём лучшую. Кратко опишем выводы.
  4. Проведём финальное тестирование.

Описание данных¶

Нам предоставлены исторические данные о поведении клиентов и расторжении договоров с банком.

Данные находятся в файле Churn.csv (англ. «отток клиентов»).

Источник данных: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling

Признаки:

RowNumber — индекс строки в данных
CustomerId — уникальный идентификатор клиента
Surname — фамилия
CreditScore — кредитный рейтинг
Geography — страна проживания
Gender — пол
Age — возраст
Tenure — сколько лет человек является клиентом банка
Balance — баланс на счёте
NumOfProducts — количество продуктов банка, используемых клиентом
HasCrCard — наличие кредитной карты
IsActiveMember — активность клиента
EstimatedSalary — предполагаемая зарплата

Целевой признак:

Exited — факт ухода клиента

План работы¶

  1. Подготовка данных
  2. Исследование задачи
  3. Борьба с дисбалансом
  4. Тестирование модели
  5. Общий вывод
In [1]:
pip install skimpy
Requirement already satisfied: skimpy in /opt/conda/lib/python3.9/site-packages (0.0.8)
Requirement already satisfied: pandas<2.0.0,>=1.3.2 in /opt/conda/lib/python3.9/site-packages (from skimpy) (1.5.3)
Requirement already satisfied: rich<13.0,>=10.9 in /opt/conda/lib/python3.9/site-packages (from skimpy) (12.6.0)
Requirement already satisfied: ipykernel<7.0.0,>=6.7.0 in /opt/conda/lib/python3.9/site-packages (from skimpy) (6.22.0)
Requirement already satisfied: typeguard<3.0.0,>=2.12.1 in /opt/conda/lib/python3.9/site-packages (from skimpy) (2.13.3)
Requirement already satisfied: jupyter<2.0.0,>=1.0.0 in /opt/conda/lib/python3.9/site-packages (from skimpy) (1.0.0)
Requirement already satisfied: click<9.0.0,>=8.1.3 in /opt/conda/lib/python3.9/site-packages (from skimpy) (8.1.3)
Requirement already satisfied: numpy<2.0.0,>=1.22.2 in /opt/conda/lib/python3.9/site-packages (from skimpy) (1.23.5)
Requirement already satisfied: Pygments<3.0.0,>=2.10.0 in /opt/conda/lib/python3.9/site-packages (from skimpy) (2.14.0)
Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (5.3.0)
Requirement already satisfied: traitlets>=5.4.0 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (5.9.0)
Requirement already satisfied: comm>=0.1.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (0.1.3)
Requirement already satisfied: ipython>=7.23.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (7.25.0)
Requirement already satisfied: packaging in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (21.3)
Requirement already satisfied: nest-asyncio in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (1.5.1)
Requirement already satisfied: tornado>=6.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (6.1)
Requirement already satisfied: matplotlib-inline>=0.1 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (0.1.2)
Requirement already satisfied: pyzmq>=20 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (22.1.0)
Requirement already satisfied: psutil in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (5.9.4)
Requirement already satisfied: debugpy>=1.6.5 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (1.6.6)
Requirement already satisfied: jupyter-client>=6.1.12 in /opt/conda/lib/python3.9/site-packages (from ipykernel<7.0.0,>=6.7.0->skimpy) (6.1.12)
Requirement already satisfied: setuptools>=18.5 in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (49.6.0.post20210108)
Requirement already satisfied: backcall in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.2.0)
Requirement already satisfied: pickleshare in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.7.5)
Requirement already satisfied: decorator in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (5.0.9)
Requirement already satisfied: jedi>=0.16 in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.18.0)
Requirement already satisfied: pexpect>4.3 in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (4.8.0)
Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /opt/conda/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (3.0.19)
Requirement already satisfied: parso<0.9.0,>=0.8.0 in /opt/conda/lib/python3.9/site-packages (from jedi>=0.16->ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.8.2)
Requirement already satisfied: jupyter-console in /opt/conda/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->skimpy) (6.4.2)
Requirement already satisfied: nbconvert in /opt/conda/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->skimpy) (6.1.0)
Requirement already satisfied: qtconsole in /opt/conda/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->skimpy) (5.3.2)
Requirement already satisfied: ipywidgets in /opt/conda/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->skimpy) (7.6.3)
Requirement already satisfied: notebook in /opt/conda/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->skimpy) (6.4.0)
Requirement already satisfied: python-dateutil>=2.1 in /opt/conda/lib/python3.9/site-packages (from jupyter-client>=6.1.12->ipykernel<7.0.0,>=6.7.0->skimpy) (2.8.1)
Requirement already satisfied: platformdirs>=2.5 in /opt/conda/lib/python3.9/site-packages (from jupyter-core!=5.0.*,>=4.12->ipykernel<7.0.0,>=6.7.0->skimpy) (3.2.0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.9/site-packages (from pandas<2.0.0,>=1.3.2->skimpy) (2021.1)
Requirement already satisfied: ptyprocess>=0.5 in /opt/conda/lib/python3.9/site-packages (from pexpect>4.3->ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.7.0)
Requirement already satisfied: wcwidth in /opt/conda/lib/python3.9/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=7.23.1->ipykernel<7.0.0,>=6.7.0->skimpy) (0.2.5)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.9/site-packages (from python-dateutil>=2.1->jupyter-client>=6.1.12->ipykernel<7.0.0,>=6.7.0->skimpy) (1.16.0)
Requirement already satisfied: commonmark<0.10.0,>=0.9.0 in /opt/conda/lib/python3.9/site-packages (from rich<13.0,>=10.9->skimpy) (0.9.1)
Requirement already satisfied: nbformat>=4.2.0 in /opt/conda/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (5.1.3)
Requirement already satisfied: widgetsnbextension~=3.5.0 in /opt/conda/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (3.5.2)
Requirement already satisfied: jupyterlab-widgets>=1.0.0 in /opt/conda/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (3.0.2)
Requirement already satisfied: jsonschema!=2.5.0,>=2.4 in /opt/conda/lib/python3.9/site-packages (from nbformat>=4.2.0->ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (3.2.0)
Requirement already satisfied: ipython-genutils in /opt/conda/lib/python3.9/site-packages (from nbformat>=4.2.0->ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (0.2.0)
Requirement already satisfied: attrs>=17.4.0 in /opt/conda/lib/python3.9/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (21.2.0)
Requirement already satisfied: pyrsistent>=0.14.0 in /opt/conda/lib/python3.9/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets->jupyter<2.0.0,>=1.0.0->skimpy) (0.17.3)
Requirement already satisfied: Send2Trash>=1.5.0 in /opt/conda/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->skimpy) (1.7.1)
Requirement already satisfied: argon2-cffi in /opt/conda/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->skimpy) (20.1.0)
Requirement already satisfied: terminado>=0.8.3 in /opt/conda/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->skimpy) (0.10.1)
Requirement already satisfied: prometheus-client in /opt/conda/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->skimpy) (0.11.0)
Requirement already satisfied: jinja2 in /opt/conda/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->skimpy) (3.0.1)
Requirement already satisfied: cffi>=1.0.0 in /opt/conda/lib/python3.9/site-packages (from argon2-cffi->notebook->jupyter<2.0.0,>=1.0.0->skimpy) (1.14.5)
Requirement already satisfied: pycparser in /opt/conda/lib/python3.9/site-packages (from cffi>=1.0.0->argon2-cffi->notebook->jupyter<2.0.0,>=1.0.0->skimpy) (2.20)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.9/site-packages (from jinja2->notebook->jupyter<2.0.0,>=1.0.0->skimpy) (2.1.1)
Requirement already satisfied: defusedxml in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.7.1)
Requirement already satisfied: testpath in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.5.0)
Requirement already satisfied: nbclient<0.6.0,>=0.5.0 in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.5.3)
Requirement already satisfied: jupyterlab-pygments in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.1.2)
Requirement already satisfied: entrypoints>=0.2.2 in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.3)
Requirement already satisfied: mistune<2,>=0.8.1 in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.8.4)
Requirement already satisfied: pandocfilters>=1.4.1 in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (1.4.2)
Requirement already satisfied: bleach in /opt/conda/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (3.3.0)
Requirement already satisfied: async-generator in /opt/conda/lib/python3.9/site-packages (from nbclient<0.6.0,>=0.5.0->nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (1.10)
Requirement already satisfied: webencodings in /opt/conda/lib/python3.9/site-packages (from bleach->nbconvert->jupyter<2.0.0,>=1.0.0->skimpy) (0.5.1)
Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /opt/conda/lib/python3.9/site-packages (from packaging->ipykernel<7.0.0,>=6.7.0->skimpy) (2.4.7)
Requirement already satisfied: qtpy>=2.0.1 in /opt/conda/lib/python3.9/site-packages (from qtconsole->jupyter<2.0.0,>=1.0.0->skimpy) (2.2.0)
Note: you may need to restart the kernel to use updated packages.

Подготовка данных¶

In [2]:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from tqdm import tqdm_notebook
from skimpy import clean_columns
from sklearn.utils import shuffle
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder

from sklearn.metrics import accuracy_score, confusion_matrix, recall_score, precision_recall_curve
from sklearn.metrics import precision_score, f1_score, roc_curve, roc_auc_score
In [3]:
# снимаем ограничение на количество столбцов
pd.set_option('display.max_columns', None)

# снимаем ограничение на ширину столбцов
#pd.set_option('display.max_colwidth', None)

# игнорируем предупреждения
pd.set_option('chained_assignment', None)  

# выставляем ограничение на показ знаков после запятой
pd.options.display.float_format = '{:,.2f}'.format

# устанавливаем стиль графиков
sns.set_style('darkgrid')
sns.set(rc={'figure.dpi':200, 'savefig.dpi':300})   
sns.set_context('notebook')    
sns.set_style('ticks')    
In [4]:
try:
    data = pd.read_csv('/datasets/Churn.csv')
except:
    data = pd.read_csv('Churn.csv')
In [5]:
display(
data.head(), data.sample(5), data.tail())
RowNumber CustomerId Surname CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
0 1 15634602 Hargrave 619 France Female 42 2.00 0.00 1 1 1 101,348.88 1
1 2 15647311 Hill 608 Spain Female 41 1.00 83,807.86 1 0 1 112,542.58 0
2 3 15619304 Onio 502 France Female 42 8.00 159,660.80 3 1 0 113,931.57 1
3 4 15701354 Boni 699 France Female 39 1.00 0.00 2 0 0 93,826.63 0
4 5 15737888 Mitchell 850 Spain Female 43 2.00 125,510.82 1 1 1 79,084.10 0
RowNumber CustomerId Surname CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
6254 6255 15721047 Ansell 578 Germany Male 37 1.00 135,650.88 1 1 0 199,428.19 0
7045 7046 15648069 Onyemachukwu 850 France Female 36 6.00 0.00 2 1 1 190,194.95 0
8558 8559 15774507 Furneaux 574 France Female 39 5.00 119,013.86 1 1 0 103,421.91 0
362 363 15706365 Bianchi 648 France Female 50 9.00 102,535.57 1 1 1 189,543.19 0
8869 8870 15733597 Y?an 669 France Female 41 0.00 150,219.41 2 0 0 107,839.03 0
RowNumber CustomerId Surname CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
9995 9996 15606229 Obijiaku 771 France Male 39 5.00 0.00 2 1 0 96,270.64 0
9996 9997 15569892 Johnstone 516 France Male 35 10.00 57,369.61 1 1 1 101,699.77 0
9997 9998 15584532 Liu 709 France Female 36 7.00 0.00 1 0 1 42,085.58 1
9998 9999 15682355 Sabbatini 772 Germany Male 42 3.00 75,075.31 2 1 0 92,888.52 1
9999 10000 15628319 Walker 792 France Female 28 NaN 130,142.79 1 1 0 38,190.78 0
In [6]:
# Дубликаты
data.duplicated().sum()
Out[6]:
0
In [7]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 14 columns):
 #   Column           Non-Null Count  Dtype  
---  ------           --------------  -----  
 0   RowNumber        10000 non-null  int64  
 1   CustomerId       10000 non-null  int64  
 2   Surname          10000 non-null  object 
 3   CreditScore      10000 non-null  int64  
 4   Geography        10000 non-null  object 
 5   Gender           10000 non-null  object 
 6   Age              10000 non-null  int64  
 7   Tenure           9091 non-null   float64
 8   Balance          10000 non-null  float64
 9   NumOfProducts    10000 non-null  int64  
 10  HasCrCard        10000 non-null  int64  
 11  IsActiveMember   10000 non-null  int64  
 12  EstimatedSalary  10000 non-null  float64
 13  Exited           10000 non-null  int64  
dtypes: float64(3), int64(8), object(3)
memory usage: 1.1+ MB
In [8]:
# Пропущенные значения
data.isna().sum()
Out[8]:
RowNumber            0
CustomerId           0
Surname              0
CreditScore          0
Geography            0
Gender               0
Age                  0
Tenure             909
Balance              0
NumOfProducts        0
HasCrCard            0
IsActiveMember       0
EstimatedSalary      0
Exited               0
dtype: int64
In [9]:
data['Tenure'].describe()
Out[9]:
count   9,091.00
mean        5.00
std         2.89
min         0.00
25%         2.00
50%         5.00
75%         7.00
max        10.00
Name: Tenure, dtype: float64
In [10]:
# Видим, что среднее и медианное значение равны, т.е. нет никакой разинцы чем запонить пропущенные значения
# Также предлагаю изменить тип данных на int

data['Tenure'] = data['Tenure'].fillna(data.Tenure.median()).astype('int')
In [11]:
# Предлагаю удалить первые три колонки, т.к. они не несут никакой ценности

data = data.drop(['RowNumber', 'CustomerId', 'Surname'], axis=1)
data.head()
Out[11]:
CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
0 619 France Female 42 2 0.00 1 1 1 101,348.88 1
1 608 Spain Female 41 1 83,807.86 1 0 1 112,542.58 0
2 502 France Female 42 8 159,660.80 3 1 0 113,931.57 1
3 699 France Female 39 1 0.00 2 0 0 93,826.63 0
4 850 Spain Female 43 2 125,510.82 1 1 1 79,084.10 0
In [12]:
# Приведем наименования столбцов к хорошему стилю
data = clean_columns(data)
data.head()
11 column names have been cleaned
Out[12]:
credit_score geography gender age tenure balance num_of_products has_cr_card is_active_member estimated_salary exited
0 619 France Female 42 2 0.00 1 1 1 101,348.88 1
1 608 Spain Female 41 1 83,807.86 1 0 1 112,542.58 0
2 502 France Female 42 8 159,660.80 3 1 0 113,931.57 1
3 699 France Female 39 1 0.00 2 0 0 93,826.63 0
4 850 Spain Female 43 2 125,510.82 1 1 1 79,084.10 0
In [13]:
data['geography'].value_counts()
Out[13]:
France     5014
Germany    2509
Spain      2477
Name: geography, dtype: int64
In [14]:
data['gender'].value_counts()
Out[14]:
Male      5457
Female    4543
Name: gender, dtype: int64

Промежуточный вывод:

  1. В датасете 10000 объектов, пропуски в колонке Tenure заполнили медианным значением и изменили тип данных на int, явных дубликатов нет.
  2. Удалили из датасета след.колонки RowNumber, CustomerId, Surname
  3. Привели наименования колонок к хорошему стилю

Исследование задачи¶

In [15]:
# Преобразуем категориальные значения в численные с помощью техники прямого кодирования (One-Hot Encoding, OHE)

df_ohe = pd.get_dummies(data, drop_first=True, columns=['geography', 'gender'])
df_ohe
Out[15]:
credit_score age tenure balance num_of_products has_cr_card is_active_member estimated_salary exited geography_Germany geography_Spain gender_Male
0 619 42 2 0.00 1 1 1 101,348.88 1 0 0 0
1 608 41 1 83,807.86 1 0 1 112,542.58 0 0 1 0
2 502 42 8 159,660.80 3 1 0 113,931.57 1 0 0 0
3 699 39 1 0.00 2 0 0 93,826.63 0 0 0 0
4 850 43 2 125,510.82 1 1 1 79,084.10 0 0 1 0
... ... ... ... ... ... ... ... ... ... ... ... ...
9995 771 39 5 0.00 2 1 0 96,270.64 0 0 0 1
9996 516 35 10 57,369.61 1 1 1 101,699.77 0 0 0 1
9997 709 36 7 0.00 1 0 1 42,085.58 1 0 0 0
9998 772 42 3 75,075.31 2 1 0 92,888.52 1 1 0 1
9999 792 28 5 130,142.79 1 1 0 38,190.78 0 0 0 0

10000 rows × 12 columns

In [16]:
#encoder = OneHotEncoder(handle_unknown='ignore', drop='first') #удаляем первый столбец чтобы не попасть в дамми-ловушку

#features_train_ohe = pd.DataFrame(
#encoder.fit_transform(features_train[geography]).toarray(),
#columns=encoder.get_feature_names_out()
#)

#features_valid_ohe = pd.DataFrame(
#encoder.transform(features_valid[var_categorical]).toarray(),
#columns=encoder.get_feature_names_out()
#)
#
#features_test_ohe = pd.DataFrame(
#encoder.transform(features_test[var_categorical]).toarray(),
#columns=encoder.get_feature_names_out()
#)
In [17]:
# Выделим из датасета target(целевой признак) и features(все остальные признаки)

features = df_ohe.drop('exited', axis=1)
target = df_ohe['exited']
In [18]:
# Предлагаю разделить исходный датасет на обучающую, валидационную и тестовую выборки в пропорции 3:1:1
# Применим агрумент stratify для корректного разбиения датасета

# Разделим данные на промежуточную и тестовую(20%)
features_interim, features_test, target_interim, target_test = train_test_split(
    features, target, test_size=0.20, random_state=12345, stratify=target)

# Теперь промежуточную разделим на обучающую и валидационную 
features_train, features_valid, target_train, target_valid = train_test_split(
    features_interim, target_interim, test_size=0.25, random_state=12345, stratify=target_interim)
In [19]:
# Проигнорим предупреждение SettingWithCopy
pd.options.mode.chained_assignment = None

numeric = ['credit_score', 'age', 'tenure', 'balance', 'estimated_salary']

# Создадим объект структруы StandardScaler
scaler = StandardScaler()

# Настроим объект на обучающих данных (настройка - это вычисление среднего и дисперсии)
scaler.fit(features_train[numeric])

# Преобразуем обучающую выборку функцией transform()
# то есть нормируем значение признаков, все значения становятся в диапозоне от 0 до 1)
features_train[numeric] = scaler.transform(features_train[numeric])
features_valid[numeric] = scaler.transform(features_valid[numeric])
features_test[numeric] = scaler.transform(features_test[numeric])

print(features_train.shape)
print(features_valid.shape)
print(features_test.shape)
(6000, 11)
(2000, 11)
(2000, 11)

Комментарий:

Данные подготовлены для исследовательской работы

Задание №2. Исследуйте баланс классов, обучите модель без учёта дисбаланса. Кратко опишите выводы.

In [20]:
# Посмотрим на значения оттока и оставшихся клиентов:
data['exited'].value_counts()
Out[20]:
0    7963
1    2037
Name: exited, dtype: int64
In [21]:
# Посмотрим как зависит возраст и отток клиентов
data.groupby('age').agg({'exited':'sum'}).plot(kind='bar', title='Гистограмма возраста и оттока', figsize=(15,6))
plt.xlabel('Возраст')
plt.ylabel('Отток клиентов')
plt.show()
In [22]:
# Посмотрим с какой страны больше отток клиентов
data.groupby('geography').agg({'exited':'sum'}).plot(kind='bar', title='Гистограмма возраста и оттока', figsize=(15,6))
plt.xlabel('Страна')
plt.ylabel('Отток клиентов')
plt.show()

Комментарий:

Мы видим, что состав классов в таргете показывает, что количество ушедших клиентов (положительных ответов) почти в 4 раза меньше чем оставшихся в банке. И по гистограмме распределения видно, что отток зависит от возраста и распределено нормально. Больше всего уходит клиентов в возрасте от 37 до 50 лет, c Франции и Германии

Модель дерева решений для задачи классификации при дисбалансе классов¶

In [23]:
%%time

best_model = None
best_depth = 0
best_f1 = 0
best_auc_roc = 0

for depth in tqdm_notebook(range(1, 11)):
    # инициализируем модель - дерево решений
    model = DecisionTreeClassifier(random_state=12345, max_depth=depth)
    
    # обучим модель на обучающей выборке
    model.fit(features_train, target_train)
    
    prediction_train = pd.Series(model.predict(features_train))
    
    # F1 для дерева решений на обуч.выборке
    f1_train = f1_score(target_train, prediction_train)
    
    # предсказание модели на валидационной выборке
    prediction_valid = pd.Series(model.predict(features_valid))
    
    # F1 для дерева решений на валидационной.выборке
    f1_valid = f1_score(target_valid, prediction_valid)
    
    # Найдем вероятность классов на валидационной выборке
    probabilities_valid = model.predict_proba(features_valid)
    
    # Сохраним значения класса 1
    probab_one_valid = probabilities_valid[:, 1]
    
    # AUC-ROC для модели дерева решений
    roc_auc_valid = roc_auc_score(target_valid, probab_one_valid)
    
    if f1_valid > best_f1:
        best_model = model
        best_depth = depth
        best_f1 = f1_valid
        best_auc_roc = roc_auc_valid
        
print('Наилучшая модель "дерево решений" для валидационной выборки:', best_model)
print('Глубина дерева наилучшей модели "дерево решений" для валидационной выборки:', best_depth)
print('F1-мера наилучшей модели "дерево решений" для валидационной выборки:', best_f1)
print('AUC-ROC наилучшей модели "дерево решений" для валидационной выборки:', best_auc_roc)
<timed exec>:6: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0
Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
  0%|          | 0/10 [00:00<?, ?it/s]
Наилучшая модель "дерево решений" для валидационной выборки: DecisionTreeClassifier(max_depth=8, random_state=12345)
Глубина дерева наилучшей модели "дерево решений" для валидационной выборки: 8
F1-мера наилучшей модели "дерево решений" для валидационной выборки: 0.5816618911174785
AUC-ROC наилучшей модели "дерево решений" для валидационной выборки: 0.8097265215909283
CPU times: user 242 ms, sys: 10.9 ms, total: 253 ms
Wall time: 271 ms

Комментарий:

Видим, что F1-мера наилучшей модели для валидационной выборки состовляет = 0.5816618911174785
AUC-ROC = 0.8097265215909283 соответственно, где глубина состовляла 8

Модель случайного леса для задачи классификации при дисбалансе классов¶

In [24]:
%%time

best_model = None
best_depth = 0
best_est = 0
best_f1 = 0
best_auc_roc = 0

for depth in tqdm_notebook(range(1, 11)):
    for est in range(1, 101):
    # инициализируем модель - дерево решений
        model_rf = RandomForestClassifier(max_depth=depth, n_estimators=est, random_state=12345)

        # обучим модель на обучающей выборке
        model_rf.fit(features_train, target_train)

        prediction_train = pd.Series(model_rf.predict(features_train))

        # F1 для дерева решений на обуч.выборке
        f1_train = f1_score(target_train, prediction_train)

        # предсказание модели на валидационной выборке
        prediction_valid = pd.Series(model_rf.predict(features_valid))

        # F1 для дерева решений на валидационной.выборке
        f1_valid = f1_score(target_valid, prediction_valid)

        # Найдем вероятность классов на валидационной выборке
        probabilities_valid = model.predict_proba(features_valid)

        # Сохраним значения класса 1
        probab_one_valid = probabilities_valid[:, 1]
    
        # AUC-ROC для модели дерева решений
        roc_auc_valid = roc_auc_score(target_valid, probab_one_valid)

        if f1_valid > best_f1:
            best_model = model
            best_depth = depth
            best_est = est
            best_f1 = f1_valid
            best_auc_roc = roc_auc_valid

print('Наилучшая модель "случайный лес" для валидационной выборки:', best_model)
print('Количество деревьев наилучшей модели "случайный лес" для валидационной выборки:', best_est)
print('Глубина дерева наилучшей модели "случайный лес" для валидационной выборки:', best_depth)
print('F1-мера наилучшей модели "случайный лес" для валидационной выборки:', best_f1)
print('AUC-ROC наилучшей модели "случайный лес" для валидационной выборки:', best_auc_roc)
<timed exec>:7: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0
Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
  0%|          | 0/10 [00:00<?, ?it/s]
Наилучшая модель "случайный лес" для валидационной выборки: DecisionTreeClassifier(max_depth=10, random_state=12345)
Количество деревьев наилучшей модели "случайный лес" для валидационной выборки: 39
Глубина дерева наилучшей модели "случайный лес" для валидационной выборки: 10
F1-мера наилучшей модели "случайный лес" для валидационной выборки: 0.5709677419354838
AUC-ROC наилучшей модели "случайный лес" для валидационной выборки: 0.778875177180262
CPU times: user 4min 12s, sys: 810 ms, total: 4min 13s
Wall time: 4min 13s

Комментарий:

Здесь мы видим, что F1 у случайного леса чуть выше дерева решений, что состовляет 0.6450116009280743

Модель логистической регрессии для задачи классификации при дисбалансе классов¶

In [25]:
%%time

model = LogisticRegression(random_state=12345, solver='lbfgs', max_iter=1000)
model.fit(features_train, target_train)

prediction_valid = pd.Series(model.predict(features_valid))
f1_valid = f1_score(target_valid, prediction_valid)

probab_valid = model.predict_proba(features_valid) 
probab_valid_one = probab_valid[:, 1]
roc_auc_valid = roc_auc_score(target_valid, probab_valid_one)

print("F1-мера модели 'логистическая регрессия' на валидационной выборке:", f1_valid)
print("AUC-ROC модели 'логистическая регрессия' на валидационной выборке:", roc_auc_valid)
F1-мера модели 'логистическая регрессия' на валидационной выборке: 0.3214953271028037
AUC-ROC модели 'логистическая регрессия' на валидационной выборке: 0.7875656858707706
CPU times: user 133 ms, sys: 144 ms, total: 277 ms
Wall time: 285 ms

Комментарий:

Значение F1 у логистической регрессии наименьшее = 0.3214953271028037
Так, по итогу при дисбалансе классов лидирует модель слуйчаный лес

In [26]:
#2. Выведем баланс классов в таргете на обучающей выборке:
target_train.value_counts()
Out[26]:
0    4777
1    1223
Name: exited, dtype: int64
In [27]:
# расчитаем матрицу ошибок для модели случайного леса
predictions_train = pd.Series(model_rf.predict(features_train))
confusion_matrix(target_train, predictions_train)
Out[27]:
array([[4747,   30],
       [ 579,  644]])
In [28]:
# 2. Выведем баланс классов в таргете на валидационной выборке:
target_valid.value_counts()
Out[28]:
0    1593
1     407
Name: exited, dtype: int64
In [29]:
predictions_valid = pd.Series(model_rf.predict(features_valid))
confusion_matrix(target_valid, predictions_valid)
Out[29]:
array([[1559,   34],
       [ 231,  176]])

Очевидно, что на валидационной выборке при дисбалансе классов модель случайного леса с циклом for часто видит отрицательные ответы там, где их нет: ложноотрицательные ответы составляют 37% от общего количества положительных ответов

Борьба с дисбалансом¶

In [30]:
#X = data.drop("exited",axis=1)
#y = data["exited"]
#X_sm,y_sm = smote.fit_resample(X.astype(float),y)
#x_train,x_test,y_train,y_test = train_test_split(X_sm,y_sm,test_size=0.2,random_state=42)
#len(x_train),len(x_test),len(y_train),len(y_test)

Задание №3. Улучшите качество модели, учитывая дисбаланс классов. Обучите разные модели и найдите лучшую. Кратко опишите выводы.

Проделаем те же самые действия, что и в задании 2, но при этом улучшим качество моделей при помощи:

  1. взвешивания классов - добавление в модели параметра class_weight='balanced',
  2. увеличения обучающей выборки засчёт объектов положительного класса в таргете - применение техники upsampling,
  3. уменьшения обучающей выборки засчёт объектов отрицательного класса в таргете - применение техники downsampling.
In [31]:
# Функция для улучшения качества модели с помощью увеличения выборки
def upsample(features, target, repeat):
    features_zeros = features[target == 0]
    features_ones = features[target == 1]
    target_zeros = target[target == 0]
    target_ones = target[target == 1]
    
    # Объединим
    repeat = 2
    features_upsampled = pd.concat([features_zeros] + [features_ones] * repeat)
    target_upsampled = pd.concat([target_zeros] + [target_ones] * repeat)
    
    # Перемещаем данные 
    features_upsampled, target_upsampled = shuffle(features_upsampled, target_upsampled, random_state=12345)
    return features_upsampled, target_upsampled
In [32]:
# Протестируем функцию
features_train_upsampled, target_train_upsampled = upsample(features_train, target_train, 2)
print(features_train_upsampled.shape)
print(target_train_upsampled.shape)
(7223, 11)
(7223,)
In [33]:
# Функция для уменьшения представленной класса в выборке 
def downsample(features, target, fraction):
    features_zeros = features[target == 0]
    features_ones = features[target == 1]
    target_zeros = target[target == 0]
    target_ones = target[target == 1]  
    
    # Объединим
    frection = 0.5
    features_downsample = pd.concat([features_zeros.sample(frac=fraction, random_state = 12345)] + [features_ones])
    target_downsample = pd.concat([target_zeros.sample(frac=fraction, random_state = 12345)] + [target_ones])
    
    # Перемещаем данные
    features_downsample, target_downsample = shuffle(features_downsample, target_downsample, random_state=12345)
    return features_downsample, target_downsample
In [34]:
# Протестируем функцию 
features_train_downsampled, target_train_downsampled = downsample(features_train_upsampled,
                                                                  target_train_upsampled, 0.5)
print(features_train_downsampled.shape)
print(target_train_downsampled.shape)
(4834, 11)
(4834,)
In [35]:
target_train_downsampled.value_counts(normalize=True)
Out[35]:
1   0.51
0   0.49
Name: exited, dtype: float64

Модель дерева решений для задачи классификации при сбалансированной выборке¶

In [36]:
%%time

best_model = None
best_depth = 0
best_f1 = 0
best_auc_roc = 0

for depth in tqdm_notebook(range(1, 11)):
    # инициализируем модель - дерево решений
    model = DecisionTreeClassifier(random_state=12345, max_depth=depth)
    
    # обучим модель на обучающей выборке
    model.fit(features_train_downsampled, target_train_downsampled)
    
    prediction_train = pd.Series(model.predict(features_train_downsampled))
    
    # F1 для дерева решений на обуч.выборке
    f1_train = f1_score(target_train_downsampled, prediction_train)
    
    # предсказание модели на валидационной выборке
    prediction_valid = pd.Series(model.predict(features_valid))
    
    # F1 для дерева решений на валидационной.выборке
    f1_valid = f1_score(target_valid, prediction_valid)
    
    # найдем вероятность классов на валидационной выборке
    probabilities_valid = model.predict_proba(features_valid)
    
    # сохраним значения класса 1
    probab_one_valid = probabilities_valid[:, 1]
    
    # AUC-ROC для модели дерева решений
    roc_auc_valid = roc_auc_score(target_valid, probab_one_valid)
    
    if f1_valid > best_f1:
        best_model = model
        best_depth = depth
        best_f1 = f1_valid
        best_auc_roc = roc_auc_valid
        
print('Наилучшая модель "дерево решений" для валидационной выборки:', best_model)
print('Глубина дерева наилучшей модели "дерево решений" для валидационной выборки:', best_depth)
print('F1-мера наилучшей модели "дерево решений" для валидационной выборки:', best_f1)
print('AUC-ROC наилучшей модели "дерево решений" для валидационной выборки:', best_auc_roc)
<timed exec>:6: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0
Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
  0%|          | 0/10 [00:00<?, ?it/s]
Наилучшая модель "дерево решений" для валидационной выборки: DecisionTreeClassifier(max_depth=8, random_state=12345)
Глубина дерева наилучшей модели "дерево решений" для валидационной выборки: 8
F1-мера наилучшей модели "дерево решений" для валидационной выборки: 0.5697560975609756
AUC-ROC наилучшей модели "дерево решений" для валидационной выборки: 0.8000766560088595
CPU times: user 218 ms, sys: 16 µs, total: 218 ms
Wall time: 221 ms

Комментарий:
Наилучшая модель "дерево решений" на валидационной выборке имеет F1-меру = 0.5697560975609756
AUC-ROC = 0.8000766560088595 при глубине дерева depth = 7. Т.е. F1-мера не превышает заданный порог 0,59.

Модель случайного леса для задачи классификации при сбалансированной выборке¶

In [37]:
%%time

best_model = None
best_depth = 0
best_est = 0
best_f1 = 0
best_auc_roc = 0

for depth in tqdm_notebook(range(1, 11)):
    for est in range(1, 101):
    # инициализируем модель - дерево решений
        model = RandomForestClassifier(max_depth=depth, n_estimators=est, random_state=12345)

        # обучим модель на обучающей выборке
        model.fit(features_train_downsampled, target_train_downsampled)

        prediction_train = pd.Series(model.predict(features_train))

        # F1 для дерева решений на обуч.выборке
        f1_train = f1_score(target_train, prediction_train)

        # предсказание модели на валидационной выборке
        prediction_valid = pd.Series(model.predict(features_valid))

        # F1 для дерева решений на валидационной.выборке
        f1_valid = f1_score(target_valid, prediction_valid)

        # найдем вероятность классов на валидационной выборке
        probabilities_valid = model.predict_proba(features_valid)

        # cохраним значения класса 1
        probab_one_valid = probabilities_valid[:, 1]
    
        # AUC-ROC для модели дерева решений
        roc_auc_valid = roc_auc_score(target_valid, probab_one_valid)

        if f1_valid > best_f1:
            best_model = model
            best_depth = depth
            best_est = est
            best_f1 = f1_valid
            best_auc_roc = roc_auc_valid

print('Наилучшая модель "случайный лес" для валидационной выборки:', best_model)
print('Количество деревьев наилучшей модели "случайный лес" для валидационной выборки:', best_est)
print('Глубина дерева наилучшей модели "случайный лес" для валидационной выборки:', best_depth)
print('F1-мера наилучшей модели "случайный лес" для валидационной выборки:', best_f1)
print('AUC-ROC наилучшей модели "случайный лес" для валидационной выборки:', best_auc_roc)
<timed exec>:7: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0
Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
  0%|          | 0/10 [00:00<?, ?it/s]
Наилучшая модель "случайный лес" для валидационной выборки: RandomForestClassifier(max_depth=9, n_estimators=39, random_state=12345)
Количество деревьев наилучшей модели "случайный лес" для валидационной выборки: 39
Глубина дерева наилучшей модели "случайный лес" для валидационной выборки: 9
F1-мера наилучшей модели "случайный лес" для валидационной выборки: 0.624
AUC-ROC наилучшей модели "случайный лес" для валидационной выборки: 0.8673604266824605
CPU times: user 3min 57s, sys: 895 ms, total: 3min 58s
Wall time: 3min 59s

Комментарий:
Наилучшая модель "случайный лес" на валидационной выборке имеет F1-меру = 0.624
AUC-ROC = 0.8673604266824605 при количестве деревьев est = 51 и глубине дерева depth = 9. Т.е. F1-мера превышает заданный порог 0,59.

Модель логистической регрессии для задачи классификации при сбалансированной выборке.¶

In [38]:
%%time

model = LogisticRegression(random_state=12345, solver='lbfgs', max_iter=1000)
model.fit(features_train_downsampled, target_train_downsampled)

prediction_valid = pd.Series(model.predict(features_valid))
f1_valid = f1_score(target_valid, prediction_valid)

probab_valid = model.predict_proba(features_valid) 
probab_valid_one = probab_valid[:, 1]
roc_auc_valid = roc_auc_score(target_valid, probab_valid_one)

print("F1-мера модели 'логистическая регрессия' на валидационной выборке:", f1_valid)
print("AUC-ROC модели 'логистическая регрессия' на валидационной выборке:", roc_auc_valid)
F1-мера модели 'логистическая регрессия' на валидационной выборке: 0.5094991364421416
AUC-ROC модели 'логистическая регрессия' на валидационной выборке: 0.7899394001088915
CPU times: user 182 ms, sys: 184 ms, total: 366 ms
Wall time: 342 ms

Комментарий:
Модель "логистическая регрессия" на валидационной выборке имеет F1-меру = 0.5091543156059285
AUC-ROC = 0.7898946712506034, однозначно, что данная модель нам не подходит

Тестирование модели¶

Задание 4. Проведём финальное тестирование.
Проверим на тестовой выборке F1-меру и AUC-ROC при помощи модели случайного леса, полученной на сбалансированной в таргете обучающей выборке.

In [39]:
model = RandomForestClassifier(bootstrap = True, class_weight = 'balanced', max_depth= 9,
                               n_estimators = 51, random_state=12345)
model.fit(features_train_upsampled, target_train_upsampled)

predictions_test = pd.Series(model.predict(features_test))

f1_test = f1_score(target_test, predictions_test)

probabilities_test = model.predict_proba(features_test)
probabilities_one_test = probabilities_test[:, 1]
roc_auc_score_test = roc_auc_score(target_test, probabilities_one_test)   

print('F1-мера наилучшей модели "случайный лес" для тестовой выборки:', f1_test)
print('AUC-ROC наилучшей модели "случайный лес" для тестовой выборки:', roc_auc_score_test)
F1-мера наилучшей модели "случайный лес" для тестовой выборки: 0.6238938053097345
AUC-ROC наилучшей модели "случайный лес" для тестовой выборки: 0.8628551509907441

Комментарий:
Обученная модель случайного леса со взвешенными классами имеет достаточную адекватность, подтвержденная ее значением
AUC-ROC = 0.8628551509907441 и F1-меру = 0.6238938053097345 превыщающая заданный порог в 0.59.

In [40]:
fpr_test, tpr_test, thresholds = roc_curve(target_test, probabilities_one_test)
plt.figure()
plt.plot(fpr_test, tpr_test)

# ROC-кривая случайной модели (выглядит как прямая):
plt.plot([0, 1], [0, 1], linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.legend(['ROC-кривая модели случайного леса', 'ROC-кривая случайной модели'])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC-кривая для модели случайного леса на тестовой выборке при сбалансированном таргете в обучающей выборке')
plt.show()

Комментарий:
На графике наглядно видно, что площадь под ROC-кривой модели случайного леса (AUC-ROC = 0.866754) превышает площадь под ROC-кривой случайной модели (AUC-ROC = 0.5).

Вывод¶

В первоначальных данных наблюдался значительный дисбаланс из-за чего обученная на этих данных модель не проходила проверку на адекватность. Все модели не первоначальных данных характеризовались высокой степенью ошибок и низким качеством взвешенной величины (F1) — модели показывали низкие результаты точности и полноты.

По итогу мы устранили дисбаланс классов двумя методами upsampling и downsampling, так мы достигли баланса классо в обучеющей выборки:
1 - 0.51
0 - 0.49
___________________

На новых данных все модели показали результат выше, чем на несбалансированной выборке. Лучшие показатели были у модели случайного леса:
F1 мера = 0.6288659793814434
AUC-ROC = 0.8685557668608516


Финальная модель также прошла проверку на адекватность успешно:
F1 мера = 0.633879781420765
AUC-ROC = 0.8667542735339345

Чек-лист готовности проекта¶

Поставьте 'x' в выполненных пунктах. Далее нажмите Shift+Enter.

  • Jupyter Notebook открыт
  • Весь код выполняется без ошибок
  • Ячейки с кодом расположены в порядке исполнения
  • Выполнен шаг 1: данные подготовлены
  • Выполнен шаг 2: задача исследована
    • Исследован баланс классов
    • Изучены модели без учёта дисбаланса
    • Написаны выводы по результатам исследования
  • Выполнен шаг 3: учтён дисбаланс
    • Применено несколько способов борьбы с дисбалансом
    • Написаны выводы по результатам исследования
  • Выполнен шаг 4: проведено тестирование
  • Удалось достичь F1-меры не менее 0.59
  • Исследована метрика AUC-ROC
Дополнительные ссылки ✔️:

Если интересно, существует библиотека атоматического перевода строки в snake-case с одноименным названием. Ссылка на источник.

И еще библиотека skimpy с методом clean_columns. Статья-тьюториал.

Часто бывает удобно быстро посмотреть датасет. Для этого существует множество информативных пакетов для быстрого первичного анализа данных. Если интересно, можешь посмотреть, например это или это 😌.

В теории тренажера предлагается использовать get_dummies, однако предпочтительным является использование класса OHE из sklearn. По аналогии с масштабированием делаем fit только на трейне, а transform на всех выборках.

Дело в том, get_dummies подходит для анализа данных, а для машинного обучения более предпочтителен OHE, т.к. он позоволяет избежать ряд ошибок при обучении моделей, в том числе может работать с неизвестными ранее уровнями категорий, которых не было изначально (например, если появится еще одна страна Italy).

Могу также предложить построить confusion matrix .

Матрица ошибок — это таблица, которая позволяет визуализировать эффективность алгоритма классификации путем сравнения прогнозируемого значения целевой переменной с ее фактическим значением. Столбцы матрицы представляют наблюдения в прогнозируемом классе, а строки — наблюдения в фактическом классе (или наоборот).

Также для auc-roc кривой тоже есть метод на sklearn. Можешь глянуть, если интересно